﻿using Microsoft.Office.InfoPath;
using System;
using System.Globalization;
using System.Reflection;
using System.Xml;
using System.Xml.XPath;

namespace InfoPathHelpers
{
    /// <summary>
    /// Provides standard methods for manipulating nodes of an InfoPathDocument.
    /// </summary>
    public static class NodeOperations
    {
        public static bool StoreEnumsAsStrings { get; set; }

        /// <summary>
        /// Sets an attribute on the supplied node. 
        /// If no attribute with the supplied local name exists a new attribute will be created.
        /// Note: Uses Prefix and NamespaceURI of the parent node.
        /// </summary>
        /// <param name="node">An XPathNavigator pointing to the node on which the attribute should be set.</param>
        /// <param name="localName">The local name of the new or existing attribute node (cannot be Null or string.Empty).</param>
        /// <param name="value">The new value of the attribute node to be set.</param>
        public static void SetAttribute(XPathNavigator node, string localName, string value)
        {
            SetAttribute(node, node.Prefix, localName, node.NamespaceURI, value);
        }

        /// <summary>
        /// Sets an attribute on the supplied node. 
        /// If no attribute with the supplied local name exists a new attribute will be created.
        /// </summary>
        /// <param name="node">An XPathNavigator pointing to the node on which the attribute should be set.</param>
        /// <param name="prefix">The namespace prefix of the new or existing attribute node (if any).</param>
        /// <param name="localName">The local name of the new or existing attribute node (cannot be Null or string.Empty).</param>
        /// <param name="namespaceURI">The namespace URI of the new or existing attribute node (if any).</param>
        /// <param name="value">The new value of the attribute node to be set.</param>
        public static void SetAttribute(XPathNavigator node, string prefix,
            string localName, string namespaceURI, string value)
        {
            if (!node.CanEdit)
                return;

            // We create a namespace resolver for selecting attibutes with a prefix.
            XmlNamespaceManager NamespaceManager = new XmlNamespaceManager(node.NameTable);
            NamespaceManager.AddNamespace(prefix, namespaceURI);

            // If the attribute doesn't exist we create it.
            if (node.SelectSingleNode("@" + prefix + ":" + localName, NamespaceManager) == null)
                node.CreateAttribute(prefix, localName, namespaceURI, value);

            // Because the default value will be used even though we specified a value 
            // in CreateAttribute() we need to set the value now.
            node.MoveToAttribute(localName, namespaceURI);
            node.SetValue(value);
            // And finally, return supplied navigator to is starting point.
            node.MoveToParent();
        }

        /// <summary>
        /// Removes the first attribute found matching the supplied localName using the NamespaceURI of the node.
        /// </summary>
        /// <param name="node">An XPathNavigator pointing to the node on which to search for the attribute.</param>
        /// <param name="localName">The local name of the new or existing attribute node (cannot be Null or string.Empty).</param>
        public static bool RemoveAttribute(XPathNavigator node, string localName)
        {
            return RemoveAttribute(node, localName, node.NamespaceURI);
        }

        /// <summary>
        /// Removes the first attribute found matching the supplied localName and namespaceURI.
        /// </summary>
        /// <param name="node">An XPathNavigator pointing to the node on which to search for the attribute.</param>
        /// <param name="localName">The local name of the new or existing attribute node (cannot be Null or string.Empty).</param>
        /// <param name="namespaceURI">The namespace URI of the new or existing attribute node (if any).</param>
        public static bool RemoveAttribute(XPathNavigator node, string localName, string namespaceURI)
        {
            if (node.MoveToAttribute(localName, namespaceURI))
            {
                node.DeleteSelf();
                return true;
            }
            return false;
        }

        /// <summary>
        /// Replaces an attribute with a new attribute with the same value.
        /// </summary>
        /// <param name="node">An XPathNavigator pointing to the node on which to search for the attribute.</param>
        /// <param name="oldAttributeName">The local name of the existing attribute node (cannot be Null or string.Empty).</param>
        /// <param name="newAttributeName">The local name of the new attribute node (cannot be Null or string.Empty).</param>
        public static void ReplaceAttribute(XPathNavigator node, string oldAttributeName, string newAttributeName)
        {
            ReplaceAttribute(node, oldAttributeName, newAttributeName, true);
        }

        /// <summary>
        /// Trys to replaces an attribute with a new attribute with the option to discard the existing value.
        /// </summary>
        /// <param name="node">An XPathNavigator pointing to the node on which to search for the attribute.</param>
        /// <param name="oldAttributeName">The local name of the existing attribute node (cannot be Null or string.Empty).</param>
        /// <param name="newAttributeName">The local name of the new attribute node (cannot be Null or string.Empty).</param>
        /// <param name="keepValue">True to copy the value from the old attribute, false to create an empty attribute.</param>
        /// <returns>False, if the old attibute did not exist or was the same as the new attribute; otherwise true.</returns>
        public static bool ReplaceAttribute(XPathNavigator node, string oldAttributeName, string newAttributeName, bool keepValue)
        {
            // If the attribute names are the same do nothing.
            if (oldAttributeName != newAttributeName)
            {
                // Get the old value.
                var value = node.GetAttribute(oldAttributeName, node.NamespaceURI);

                // Remove the old attribute.
                if (RemoveAttribute(node, oldAttributeName))
                {
                    // Add a new attribute with the value of the old one if desired.
                    SetAttribute(node, newAttributeName, keepValue ? value : string.Empty);
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Removes the xsi:nil attribute from non-string fields.
        /// </summary>
        /// <param name="node">An XPathNavigator pointing to the node on which the attribute should be removed.</param>
        public static void RemoveNilAttribute(XPathNavigator node)
        {
            RemoveAttribute(node, "nil", "http://www.w3.org/2001/XMLSchema-instance");
        }

        /// <summary>
        /// Adds the xsi:nil attribute.
        /// </summary>
        /// <param name="node">An XPathNavigator pointing to the node to which the attribute should be added.</param>
        public static void SetNilAttribute(XPathNavigator node)
        {
            SetAttribute(node, "xsi", "nil", "http://www.w3.org/2001/XMLSchema-instance", "true");
        }

        /// <summary>
        /// Selects as single node from MainDataSource of the supplied <paramref name="form"/>,
        /// using the supplied <paramref name="xPath"/> query.
        /// </summary>
        /// <param name="form">The form to query for the node.</param>
        /// <param name="xPath">The path of the node.</param>
        /// <returns>An XPathNavigator pointing to the first node mathing the supplied 
        /// <paramref name="xPath"/> query; or null if it is not found</returns>
        public static XPathNavigator GetNavigator(IDataSourceContext context, string xPath)
        {
            return context.SelectSingleNode(xPath);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to the Type specified as <paramref name="T"/>.
        /// <paramref name="xPath"/> 
        /// </summary>
        /// <typeparam name="T">The type to be returned.</typeparam>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <returns>The value of the element as the Type <paramref name="T"/>. 
        /// Or, if the element does not exist or the value can not be parsed then default(T).</returns>
        public static T GetValue<T>(IDataSourceContext context, string xPath)
        {
            T Value;
            TryGetValue(GetNavigator(context, xPath), out Value);
            return Value;
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="T"/>.
        /// </summary>
        /// <typeparam name="T">The type to be returned.</typeparam>
        /// <param name="node">The node to convert.</param>
        /// <returns>The value of the element as the Type <paramref name="T"/>. 
        /// Or, if the element does not exist or the value can not be parsed then default(T).</returns>
        public static T GetValue<T>(XPathNavigator node)
        {
            T Value;
            TryGetValue(node, out Value);
            return Value;
        }

        /// <summary>
        /// Converts the value of the supplied node to the type of <paramref name="result"/> the return value indicates if conversion succeeded.
        /// Note: Can be used for all CLR types with TryParse as well as Enums. Custom types must implement a TryParse method to work.
        /// </summary>
        /// <typeparam name="T">The type of value to try and get from the node.</typeparam>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion succeeded or the default value of <typeparamref name="T"/>.</param>
        /// <returns>A boolean value indicating whether or not conversion was successful.</returns>
        public static bool TryGetValue<T>(IDataSourceContext context, string xPath, out T result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and returns the value.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if it could be located.</param>
        /// <returns>True is the node could be found; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out string result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to an int?.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the null.</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out int? result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to a DateTime?.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the null.</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out DateTime? result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to a bool?.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or null.</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out bool? result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to a double?.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or null.</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out double? result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to an int.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(int).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out int result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to a bool.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(bool).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out bool result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to a double.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(double).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out double result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to a DateTime.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(DateTime).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out DateTime result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Uses the supplied <paramref name="xPath"/> to locate an element in the <paramref name="context"/> 
        /// and converts it to a Version.
        /// </summary>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(Version).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(IDataSourceContext context, string xPath, out Version result)
        {
            return TryGetValue(GetNavigator(context, xPath), out result);
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="T"/>.
        /// The return value indicates if conversion succeeded.
        /// Note: Can be used for all CLR types with TryParse as well as Enums. Custom types must implement a TryParse method to work.
        /// </summary>
        /// <typeparam name="T">The type of value to try and get from the node.</typeparam>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion succeeded or the default value of <typeparamref name="T"/>.</param>
        /// <returns>A boolean value indicating whether or not conversion was successful.</returns>
        public static bool TryGetValue<T>(XPathNavigator node, out T result)
        {
            // If there is no node to get a value from then conversion fails automatically.
            // An exception is not thrown because this method provides checking via the returned value.
            if (node == null)
            {
                result = default(T);
                return false;
            }

            var Type = typeof(T);

            if (Type == typeof(string))
            {
                result = (T)(object)node.Value;
                return true;
            }

            if (Type == typeof(Version))
            {
                Version Version;
                if (TryGetValue(node, out Version))
                {
                    result = (T)(object)Version;
                    return true;
                }
                result = default(T);
                return false;
            }

            // We need to use the parse method of the underlying type if T is nullable.
            Type = Nullable.GetUnderlyingType(typeof(T)) ?? Type;

            if (Type.IsEnum)
            {
                // Enum types do not have TryParse methods so if T is an enum then we
                // use Enum.Parse but to avoid exceptions we first check if the value is defined.
                // TODO: If re-targeting .net framework to version 4.0+ in future use Enum.TryParse.
                var EnumValues = node.Value.Split(',');
                for (var i = 0; i < EnumValues.Length; i++)
                {
                    string Value = EnumValues[i].Trim();
                    int ValueAsInt;

                    if (!(Enum.IsDefined(Type, Value) ||
                        (int.TryParse(Value, out ValueAsInt) && Enum.IsDefined(Type, ValueAsInt))))
                    {
                        result = default(T);
                        return false;
                    }
                }
                result = (T)Enum.Parse(Type, node.Value.ToString());
                return true;
            }

            MethodInfo TryParse = Type.GetMethod("TryParse", new[] { typeof(string), Type.MakeByRefType() });
            if (TryParse == null)
            {
                throw new InvalidOperationException("The supplied type cannot be parsed");
            }

            T Result = default(T);
            var Parameters = new object[] { node.Value, Result };
            bool Success = (bool)TryParse.Invoke(null, Parameters);
            if (Success)
            {
                Result = (T)Parameters[1];
            }
            result = Result;
            return Success;
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if it could be located.</param>
        /// <returns>True is the node could be found; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out string result)
        {
            result = node.Value;
            return true;
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the null.</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out int? result)
        {
            int Result;

            if (string.IsNullOrEmpty(node.Value))
            {
                result = null;
                return true;
            }

            if (TryGetValue(node, out Result))
            {
                result = Result;
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the null.</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out DateTime? result)
        {
            if (string.IsNullOrEmpty(node.Value))
            {
                result = null;
                return true;
            }

            DateTime Result;
            if (TryGetValue(node, out Result))
            {
                result = Result;
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or null.</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out bool? result)
        {
            if (string.IsNullOrEmpty(node.Value))
            {
                result = null;
                return true;
            }

            bool Result;
            if (TryGetValue(node, out Result))
            {
                result = Result;
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or null.</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out double? result)
        {
            if (string.IsNullOrEmpty(node.Value))
            {
                result = null;
                return true;
            }

            double Result;
            if (TryGetValue(node, out Result))
            {
                result = Result;
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(int).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out int result)
        {
            return int.TryParse(node.Value, out result);
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(double).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out bool result)
        {
            return bool.TryParse(node.Value, out result);
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(double).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out double result)
        {
            return double.TryParse(node.Value, out result);
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(Version).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out DateTime result)
        {
            return DateTime.TryParse(node.Value, out result);
        }

        /// <summary>
        /// Converts the value of the supplied node to the Type specified as <paramref name="result"/>.
        /// The return value indicates if conversion succeeded.
        /// </summary>
        /// <param name="node">The node to try and get the value from.</param>
        /// <param name="result">When this method returns result contains the value of the node if conversion suceeded or the default(Version).</param>
        /// <returns>True is conversion was successful; otherwise false.</returns>
        public static bool TryGetValue(XPathNavigator node, out Version result)
        {
            // Version can be quite useful for forms so I have provided support
            // for getting a value as a Version. Unfortunately there is no built in
            // parsing for Version so instead the code performs 
            if (string.IsNullOrEmpty(node.Value))
            {
                result = null;
                return false;
            }

            var VersionComponents = node.Value.Split('.');
            var Length = VersionComponents.Length;
            if (Length < 2 || Length > 4)
            {
                result = null;
                return false;
            }

            int x;
            for (int i = 0; i < Length; i++)
            {
                if (!int.TryParse(VersionComponents[i], out x) || x < 0)
                {
                    result = null;
                    return false;
                }
            }

            result = new Version(node.Value);
            return true;
        }

        /// <summary>
        /// Sets the value of a node to the best fitting string representation of the supplied <paramref name="value"/>.
        /// </summary>
        /// <typeparam name="T">The type of the value to be set.</typeparam>
        /// <param name="context">The datasource in which to look for the element.</param>
        /// <param name="xPath">The XPath used to locate the element in the datasource.</param>
        /// <param name="value">The value to set.</param>
        /// <param name="isNillable">True for nillable nodes; otherwise false. (This will set the "xsi:nil" attribute automatically.</param>
        public static void SetValue<T>(IDataSourceContext context, string xPath, T value, bool isNillable)
        {
            SetValue(GetNavigator(context, xPath), value, isNillable);
        }

        /// <summary>
        /// Sets the value of a node to the best fitting string representation of the supplied <paramref name="value"/>.
        /// </summary>
        /// <typeparam name="T">The type of the value to be set.</typeparam>
        /// <param name="node">An XPathNavigator pointing to the node to set the value on.</param>
        /// <param name="value">The value to set.</param>
        /// <param name="isNillable">True for nillable nodes; otherwise false. (This will set the "xsi:nil" attribute automatically.</param>
        public static void SetValue<T>(XPathNavigator node, T value, bool isNillable)
        {
            if (node == null)
            {
                throw new ArgumentNullException("node");
            }

            if(node.HasChildren && node.InnerXml.Contains("<"))
            {
                // If the node has children and the InnerXml contains a opening tag then it has
                // child elements and the value should not be set.
                throw new ArgumentException("The supplied node referenced a group. Value cannot be set on groups", "node");
            }

            if (value == null || string.IsNullOrEmpty(value.ToString()))
            {
                // For null or empty values we just set the node to an empty string and 
                // set the nil attribute if necessary.
                node.SetValue(string.Empty);
                if (isNillable)
                {
                    SetNilAttribute(node);
                }
                return;
            }

            // Remove the nil attribute (this method works whether it exists or not).
            RemoveNilAttribute(node);

            node.SetValue(Convert(value, StoreEnumsAsStrings));
        }

        public static string Convert<T>(T value, bool storeEnumsAsStrings)
        {
            var ConvertedValue = string.Empty;

            if (value is DateTime)
            {
                // For DateTime values we need to store them in a specific format for them to
                // be recognized as valid.
                ConvertedValue = ((DateTime)(object)value).ToString("s", CultureInfo.CurrentCulture);
            }
            else if (!storeEnumsAsStrings && value is Enum)
            {
                // If not storing enums as strings we need to cast them to int first.
                ConvertedValue = ((int)(object)value).ToString();
            }
            else if (value is bool)
            {
                // Boolean values are stored in lower case.
                ConvertedValue = value.ToString().ToLower();
            }
            else
            {
                // Everything else is just stored as it's string representation.
                ConvertedValue = value.ToString();
            }

            return ConvertedValue;
        }

        /// <summary>
        /// Replaces the <paramref name="node"/> with an empty node.
        /// </summary>
        /// <param name="node">The node to clear.</param>
        public static void ClearNode(XPathNavigator node)
        {
            string EmptyNode = string.Format("<{0}:{1} xmlns:{0}=\"{2}\"></{0}:{1}>",
                node.Prefix,node.LocalName, node.NamespaceURI);
            node.ReplaceSelf(EmptyNode);
        }
    }
}
